Руководство по поиску функциональных ошибок
Цель
При выполнении лабораторных работ вы непременно будете сталкиваться с множеством ошибок. И это нормально: "Не ошибается тот, кто ничего не делает" — © Джейсон Стейтем.
Важно воспитать в себе положительное восприятие обнаружения ошибок (ведь это приводит к улучшению вашего творения). Если относиться к обнаружению ошибок отрицательно, то вы подсознательно будете пытаться найти ошибки спустя рукава, но, если вы "в домике", и ошибок не видите — это не значит, что их нет.
При должном отношении, поиск ошибок может превратиться в увлекательное детективное расследование, где у вас есть "место преступления" (обнаруженное несоответствие в поведении, обычно это не сама ошибка, а ее следствие, круги на воде) и какой-то "набор улик" (фрагменты лога, исходный код). И вы, по чуть-чуть, будете разматывать "нераспутываемую паутину лжи", получая всё новые улики, ведущие к истинной ошибке.
Этот документ представляет собой практикум по поиску подобных ошибок в SystemVerilog-коде.
Обратите внимание на то, как ставится ударение в словосочетании "временна́я диаграмма" (не "вре́менная"). В обиходе это словосочетание заменяется словом "времянка".
- Руководство по поиску функциональных ошибок
- Цель
- Алгоритм поиска ошибок
- Работа с логом при появлении ошибок
- Поиск ошибки на временной диаграмме
- Открытие файла исходного кода проблемного сигнала
- Добавление сигналов объектов на временную диаграмму
- Сброс симуляции и ее повтор, установка времени моделирования
- Исправление сигналов с Z-состоянием
- Поиск ошибки в сигналах, формирующих проблемный сигнал
- Исправление логики проблемного сигнала
- Проблема необъявленных сигналов
- Самостоятельная работа
Алгоритм поиска ошибок
- Обычно всё начинается с сообщения в логе тестов (никто не проверяет глазами временную диаграмму сложных проектов, состоящую из тысяч сигналов, меняющихся миллионы раз за микросекунду), но на наших лабораторных работах с относительно простыми модулями, этот шаг иногда может быть и пропущен.
Сообщение в логе обычно содержит следующую ключевую информацию: имя сигнала, на котором установилось неверное значение, и время, когда это произошло. Чем лучше написано верификационное окружение, тем больше ключевой информации будет отражено в сообщении, поэтому его написание является своего рода искусством. - Получив имя сигнала и время, мы отправляемся на временную диаграмму и проверяем нашу ошибку. Как это сделать? Необходимо определить по коду, какие сигналы и каким образом управляют нашим сигналом. Вариантов может быть несколько:
- Управляющие сигналы имеют корректное значение, но логика, по которой они управляют сигналом неверна, из-за этого на нем возникает неверное значение. Это идеальный случай, при возникновении которого мы сразу же находим причину проблемы и исправляем ее.
- Логика управления верна, а какая-то часть управляющих сигналов имеет неверное значение (пусть для примера, неверное значение будет на управляющем сигнале
X
). Это означает, что обнаруженное несоответствие сигналов является уже следствием какой-то ошибки, и мы должны вернуться к шагу 2, проверяя источники для сигнала со значениемX
. Так происходит до тех пор, пока мы не попадаем в тип 1. - Логика управления и значения управляющих сигналов верны. Это самый сложный тип ошибок, который заключается либо в ошибке в спецификации разрабатываемого устройства, либо в САПРе или компонентах, влияющих на его работу. В рамках данного курса вас не должны заботить данные ошибки, и при их возникновении вам стоит обратиться к преподавателю (предварительно убедившись, что ошибка совершенно точно не подходит под первые два варианта).
- Любая возможная комбинация всех предыдущих типов.
- Обнаружив первопричину ошибки, мы исправляем ее (возможно дополняя набор тестов, или внеся правки в спецификацию), и повторно запускаем все тесты, чтобы убедиться в двух вещах:
- ошибка действительно исправлена
- исправление ошибки не породило новых ошибок
Давайте отработаем эти шаги на примере отладки ошибок в проекте по вычислению приблизительной длины вектора, создание которого было описано в документе "Менеджер проекта".
Работа с логом при появлении ошибок
После запуска симуляции мы видим в логе множество ошибок:
Рисунок 1. Пример сообщения об ошибках в тесте.
В любой ситуации с множеством ошибок, сначала надо разбираться с самой первой из них, поскольку она может быть причиной появления всех остальных. Поэтому листаем лог до момента первой ошибки:
Рисунок 2. Пример конкретной ошибки в тесте.
В логе сказано, что в момент времени 5ns
, на вход схемы подавались координаты вектора, равные 0
и 0
, модель посчитала, что длина вектора равна нулю, в то время как схема вернула значение x
.
Поиск ошибки на временной диаграмме
Давайте найдем это место на временной диаграмме. Обычно, сразу после запуска симуляции на временной диаграмме отображено место, где симуляция остановилась (возможно с очень неподходящим масштабом). Для начала подгоним масштаб таким образом, чтобы вся временная диаграмма умещалась в окне. Это делается либо нажатием правой кнопкой мыши по в области отображения сигналов, с выбором "Full View" во всплывающем меню, либо нажатием соответствующей кнопки на панели временной диаграммы (см. рис. 4), либо нажатием комбинации клавиш Ctrl+0
. Затем найдем приблизительное место рядом с тем временем, что нас интересует, установим там курсор, и приблизим масштаб (покрутив колесиком мыши при зажатой клавише Ctrl
), периодически уточняя местоположения курсора, пока не найдем интересующее нас место.
Рисунок 3. Пример временной диаграммы сразу поле остановки моделирования.
Рисунок 4. Пример установки масштаба временной диаграммы таким образом, чтобы та помещалась в текущем окне.
Рисунок 5. Пример временной диаграммы после подгонки масштаба.
Рисунок 6. Установка курсора в начало моделирования, чтобы, при увеличении масштаба, временная диаграмма сходилась к началу.
Рисунок 7. Временная диаграмма, отмасштабированная к времени ошибки с рис. 2.
Мы видим ровно ту информацию, которую нам предоставил тестбенч. Теперь надо разобраться в причинах возникновения X-состояния. Такое может произойти по множеству причин, вот три из них:
- какой-то из сигналов, формирующих этот находится в
X
илиZ
состоянии; - два каких-то сигнала одновременно пытаются выставить разные значения на целевой сигнал;
- этот сигнал является выходом модуля, но был описан с ключевым словом
input
.
Открытие файла исходного кода проблемного сигнала
В любом случае, первым делом необходимо определить, источник формирования значения сигнала res
. Откроем файл с исходным кодом, где определен данный сигнал. Для этого, нажмем правой кнопкой мыши по имени сигнала на временной диаграмме, и выберем Go To Source Code
:
Рисунок 8. Переход к месту объявления "проблемного" сигнала.
Откроется код, представленный в листинге 1 (с курсором на строчке logic [31:0] res;
):
module tb_vector_abs();
logic [31:0] a;
logic [31:0] b;
logic [31:0] res;
vector_abs dut(
.x(a),
.y(b),
.abs(res)
);
//...
Листинг 1. Начало кода симулируемого тестбенча.
Выделив res
мы видим, что у нас подсветился res
в строке abs(res)
. Это означает, что мы завели наш провод внутрь объекта dut
модуля vector_abs
, и у нас проблема второго типа (в логике работы провода res
нет ошибок, он принял некорректное значение, поскольку ему таковое передали).
В этом можно убедиться, если вытащить сигналы модуля vector_abs
на временную диаграмму. Чтобы это сделать, надо переключиться на окно Scope
, где размещена иерархия моделируемых объектов.
Добавление сигналов объектов на временную диаграмму
Обратите внимание, что в иерархии окна
Scope
находятся не имена модулей, а имена сущностей модуля. В приведенном выше листинге кода мы создали сущность модуляvector_abs
с именемdut
, поэтому в иерархииScope
мы видим внутри модуля верхнего уровня объектdut
(неvector_abs
), так будет и со всеми вложенными объектами.
Выделим объект dut
. В окне Objects
справа отобразятся все внутренние сигналы (входы/выходы, внутренние провода и регистры) объекта dut
:
Рисунок 9. Отображение внутренних сигналов проверяемого модуля.
Вообще говоря, мы уже видим, что выход abs
(к которому подключен наш провод res
) находится в X-состоянии, но для отработки навыков, разберемся с добавлением новых сигналов на временную диаграмму. Можно поступить двумя способами:
- Добавить все сигналы (то, что видно в окне
Objects
на временную диаграмму) из окнаScope
для этого, либо перетаскиваем нужный нам объект, зажав левую кнопку мыши на временную диаграмму, либо жмем правой кнопкой мыши по нужному объекту, и выбираемAdd to Wave Window
- Добавить отдельные сигналы из окна
Objects
. Для этого выделяем их (возможно множественное выделение через модификаторыshift
илиctrl
), и как и в прошлом случае, либо перетаскиваем сигналы левой кнопкой мыши, либо добавляем их через правую кнопку мыши.
Рисунок 10. Добавление сигналов модуля на временную диаграмму.
Рисунок 11. Результат добавления сигналов модуля на временную диаграмму.
По мере роста сложности проекта, число сигналов на временной диаграмме будет постоянно расти, в связи с чем встает вопрос группировки сигналов.
Для того чтобы объединить сигналы в группу, необходимо их выделить. Это можно сделать двумя способами:
- кликнув левой кнопкой мыши по каждому из интересующих сигналов при зажатой клавише
Ctrl
; - если речь идет о диапазоне сигналов, можно выбрать сигнал с одного края, после чего, при зажатой клавише
Shift
, выбрать сигнал с другого края этого диапазона.
После выбора, необходимо нажать правой кнопкой мыши по выделенным сигналам, и в низу выпадающего списка выбрать New Group
.
Рисунок 12. Пример создания группы сигналов (контекстное меню было обрезано для удобства отображения).
После создания группы, ей нужно будет дать имя. В случае, если все сигналы принадлежат одному модулю, удобно называть группу сигналов именем этого модуля.
Рисунок 13. Пример созданной группы сигналов.
Данную группу можно сворачивать и разворачивать, нажимая на соответствующую стрелку слева от имени группы.
Обратите внимание, что часть сигналов отображают какое-то значение (сигнал
abs
отображает X-состояние), а часть не отображают ничего. Так произошло, потому что проводabs
непрерывно связан с проводомres
, с точки зрения симулятора это одна сущность, и записывая во время моделирования значения для сигналаres
, симулятор неявно записывал значения для сигналаabs
, чего не скажешь про остальные сигналы, которых не было во время моделирования на временной диаграмме.
Сброс симуляции и ее повтор, установка времени моделирования
Для того, чтобы получить отсутствующие значения, необходимо повторить моделирование. Для этого, необходимо сбросить время моделирования в 0 и запустить его снова.
Для этого, необходимо на панели симуляции нажать кнопку Restart
(|◀
), а затем кнопку Run all
(▶
) или Run for
(▶t
). Положение кнопок в окне Vivado иллюстрирует рис. 14.
Рисунок 14. Расположение кнопок, управляющих моделированием в окне Vivado.
Панель управления симуляции с кнопками:
Restart
, горячие клавиши:Ctrl+Shift+F5
;Run all
, горячая клавиша:F3
;Run for
, горячие клавиши:Shift+F2
;Relaunch Simulation
.
Run for
выполняет моделирование указанного количества времени, после чего моделирование приостанавливается. Моделирование может быть остановлено так же и вручную, либо вызовом соответствующей инструкции из кода теста.
Run all
отличается от Run for
тем, что в качестве количества моделируемого времени указывается "бесконечность", и моделирование будет остановлено только вручную, либо вызовом соответствующей инструкции.
Обратите внимание, что для добавления недостающих значений добавленных сигналов лучше всего выполнять описанную выше инструкцию. Аналогичного результата можно добиться и нажатием на кнопку
Relaunch Simulation
, однако эта команда работает дольше и, если вы не меняли исходный код модулей, не нужна.
Кроме того, чтобы курсор и лог снова не ушли далеко от места первой ошибки, можно сразу указать, необходимое нам время моделирования перед выполнением команды Run for
: 5ns
.
Рисунок 15. Пример моделирования 5ns.
На рис. 16 представлен результат моделирования с новыми сигналами.
Рисунок 16. Результат повторного моделирования после добавления на временную диаграмму новых сигналов.
Видим два сигнала в Z-состоянии и один сигнал в X-состоянии. Обычно, сигналы с Z-состоянием проще всего исправить, т.к. зачастую это забытое или некорректное подключение провода. Кроме того, сигнал, зависящий от сигнала с Z-состоянием, может оказаться в X-состоянии, так что это может быть решением нашей проблемы, поэтому проверим провода min
и min_half
. Сперва займемся сигналом min
и перейдем к шагу 2 нашего алгоритма (нажимаем правой кнопкой мыши и выбираем Go To Source Code
):
module vector_abs(
input logic [31:0] x,
input logic [31:0] y,
output logic [31:0] abs
);
logic [31:0] min;
logic [31:0] min_half;
max_min max_min_unit(
.a(x),
.b(y),
.max(max),
.min(min)
);
//...
Исправление сигналов с Z-состоянием
Мы видим, что сигнал min
подключен к выходу min
объекта max_min_unit
модуля max_min
. Добавим сигналы этого модуля на временную диаграмму. Для этого, необходимо раскрыть список объектов, содержащихся в объекте dut
иерархии объектов Scope
и выбрать там объект max_min_unit
.
Рисунок 17. Добавление сигналов вложенных модулей на временную диаграмму.
Добавляем внутренние сигналы на временную диаграмму, группируем их под именем max_min
, и повторяем моделирование.
Рисунок 18. Результат добавления и группировки сигналов подмодуля max_min
.
Произошло что-то странное: все внутренние сигналы объекта max_min_unit
"зеленые" (не имеющие X или Z состояния), однако подключенный к выходу этого модуля сигнал min
находится в Z-состоянии. Как такое могло произойти?
Если присмотреться к сигналу min
, находящемуся в Z-состоянии, можно заметить, что младшая цифра находится не в Z-состоянии, а в состоянии 0
, такое же значение стоит и на сигнале min
объекта max_min_unit
. Это интересно.
Если присмотреться к этим двум сигналам еще пристальней, то можно увидеть, что у сигнала min
объекта dut
разрядность 32 бита, в то время как разрядность сигнала min
объекта max_min_unit
составляет 4 бита.
Это и является проблемой: мы подключили 4 бита 4-разрядного сигнала min
к младшим 4 битам 32-разрядного сигнала min
, а остальные разряды остались не подключенными.
По всей видимости, при написании модуля max_min
, была указана неверная разрядность сигнала min
, вместо 31
было написано 3
. Исправим это и повторим моделирование.
Обратите внимание, что поскольку мы изменили исходный код, в этот раз необходимо нажать на кнопку
Relaunch Simulation
, поскольку нужна повторная компиляция проекта.
Рисунок 19. Результат моделирования после исправления разрядности сигнала min
.
В логе сообщается о 102 найденных ошибках. Ровно на одну ошибку меньше, чем было ранее. Это не означает, что в проекте осталось 102 ошибки, только то, что, исправив данную ошибку — мы действительно что-то исправили, и один из тестовых сценариев, который ранее завершался ошибкой, теперь завершился без неё.
Помните, что если в проекте много ошибок, то часть ошибок может выправлять поведение других ошибок (хоть и не всегда, но иногда минус на минус может выдать плюс контексте ошибок проекта), поэтому надо осторожно полагаться на число найденных ошибок, если это число больше нуля.
Посмотрим на нашу временную диаграмму снова, и выберем дальнейшие действия:
Рисунок 20. Временная диаграмма после исправления разрядности сигнала min
.
Мы видим, что на временной диаграмме не осталось сигналов в X или Z-состоянии, а значит мы собрали все "низковисящие" улики нашего с вами расследования. Вернемся к месту преступления и попробуем поискать новые улики:
Рисунок 21. Первая ошибка в новом логе моделирования.
Поиск ошибки в сигналах, формирующих проблемный сигнал
Мы видим, что первой ошибкой в логе стала не та ошибка, что была прежде. Раньше первый неверный результат мы видели в момент времени 5ns
, когда на схему подавались значения 0
и 0
, теперь же первой ошибкой стал момент времени 10ns
, когда на схему подаются значения 1
и 1
. Наше устройство считает, что результат должен равняться 3
, в то время как модель считает, что результат должен равняться 1
. Проверим, нет ли ошибки в модели и посчитаем результат самостоятельно:
Для определения приблизительной длины вектора в евклидовом пространстве (т.е. длины гипотенузы прямоугольного треугольника, которая равна квадратному корню из суммы квадратов катетов) можно воспользоваться формулой:
sqrt(a^2 + b^2) ≈ max + min/2
, где max
и min
— большее и меньшее из пары чисел соответственно [Ричард Лайонс: Цифровая обработка сигналов, стр. 475].
Подставим наши числа в формулу (поскольку оба числа равны, не важно какое из них будет максимумом, а какое минимумом):
1 + 1/2 = 1.5
Ни модель, ни схема не правы?
На самом деле, наше устройство поддерживает только целочисленную арифметику, поэтому результат будет:
1 + 1/2 = 1 + 0 = 1
Модель правильно отразила особенность нашего устройства и дала корректный результат.
Значит надо смотреть как формируется результат в нашем устройстве. Посмотрим на выход abs
в модуле vector_abs
:
assign abs = max + min_half;
Выход abs
зависит от двух внутренних сигналов: max и min_half
. В соответствии с нашим алгоритмом, либо проблема в логике, связывающей эти два сигнала (операции сложения), либо в значении какого-то из этих сигналов, либо комбинации этих вариантов.
Изучив модуль, мы понимаем, что в логике этого присваивания проблем нет, т.к. оно повторяет логику формулы max + min/2
, складывая максимум с половиной минимума. Значит проблема в значении какого-то из этих сигналов (или обоих из них). Посчитаем значения этих сигналов самостоятельно (для сложного проекта эти значения посчитала бы модель):
1
и 0
.
Смотрим, какие значения установлены на сигналах max
и min_half
в момент времени 10ns
.
Рисунок 22. Значения сигналов max
и min_half
в момент времени 10 ns
(интересующие нас сигналы выделены зелёным)
Обратите внимание: вы можете менять цвета сигналов временной диаграммы через контекстное меню выделенных сигналов.
Мы видим, что в момент времени 10 ns
значения max
и min_half
изменились как 1 -> 4
и 2 -> 8
соответственно. Нас интересуют значения 1
и 2
, т.к. в момент времени 10ns
на выходе схемы в этот момент был установившийся результат для предыдущих значений (еще не успел посчитаться результат для новых значений).
Значение max=1
совпадает с ожидаемым, в то время как min_half=2
явно нет.
Мы нашли причину неправильного вычисления результата: и правда, 1+2=3
, теперь необходимо найти ошибку в вычислении сигнала min_half
.
Как и с сигналом abs
, необходимо определить сигналы, влияющие на значение сигнала min_half
. Данный сигнал подключен к выходу quotient
модуля half_divider
, поэтому мы будем смотреть исходный код данного модуля:
module half_divider(
input [31:0] numerator,
output[31:0] quotient
);
assign quotient = numerator << 1'b1;
endmodule
Что делает данный модуль? Он принимает на вход значение и делит его на два. На вход данного модуля будет приходить значение минимума из нашей формулы.
Выход данного модуля зависит от входа numerator
и логики сдвига влево на 1. Это значит, что проблема либо в логике, либо в значении, подаваемом на вход. Выведем сигнал numerator
на временную диаграмму и посмотрим на его значение в момент времени `10ns.
Рисунок 23. Значение сигнала numerator
в момент времени 10 ns
.
Мы помним, что в момент, когда схема начала выдавать неправильный результат, на его входы подавались числа 1
и 1
, это значит, что на вход numerator
пришло корректное значение: минимум из этих двух чисел и правда равен 1
. Проверим логику данного модуля.
Исправление логики проблемного сигнала
Операция деления в цифровой схемотехнике является очень "дорогой" в плане ресурсов логических блоков и критического пути, поэтому этой операции часто стараются избегать. В нашем случае, нам не нужно обычное деление — нам нужно деление только напополам. В двоичной арифметике, для того чтобы разделить число на два, достаточно отбросить его младшую цифру. Вы часто пользуетесь подобной операцией в повседневной жизни при выполнении операции деления на 10: отбрасываете младшую цифру в десятичной арифметике.
Именно поэтому, когда мы в первый раз пытались посчитать результат "на бумаге", у нас было расхождение с моделью: когда мы делим 1 на 2, мы получаем 0.5, однако деление путем отбрасывания цифры округляет результат вниз (1/2=0, 15/10=1).
Как "отбросить" цифру средствами цифровой логики? Для этого используется операция сдвига вправо.
Операция сдвига вправо в SystemVerilog записывается оператором >>
. Справа от оператора указывается число "отбрасываемых цифр", в нашем случае одна. Но постойте, в логике присваивания стоит оператор <<
. Это ошибка, исправим ее!
Повторяем моделирование.
Рисунок 24. Результат моделирования после исправления оператора сдвига.
Снова на одну ошибку меньше. Не унываем, вряд ли в проекте число ошибок больше, чем число непустых строк самого проекта. Возвращаемся к начальной ошибке:
Рисунок 25. Первая ошибка в повторном моделировании.
Мы продвинулись во времени безошибочного моделирования до 15 ns
, начинаем наше расследование с начала:
На вход схемы подаются значения 3
и 4
, схема считает, что результатом вычисления max + min/2
будет 2
, модель считает, что 5
. Посчитаем сами:
max=4
min=3
max + min/2 = 4 + 3/2 = 4 + 1 = 5
И снова модель выдала правильный результат. Разберемся в значениях сигналов, формирующих сигнал abs
.
Проблема необъявленных сигналов
К этому моменту на вашей временной диаграмме скорей всего стало уже очень много сигналов. Уберем лишние, оставив только внутренние сигналы модуля vector_abs
(для этого выделяем ненужные сигналы, и удаляем их с помощью клавиши Delete
).
Рисунок 26. Поведение внутренних сигналов модуля vector_abs
на временной диаграмме.
В глаза сразу же бросается, что сигнал max
внешне отличается от всех остальных — он ведет себя как однобитный сигнал. Если все остальные сигналы 32-разрядные, то и сигнал max
должен быть таким же. Перейдем к объявлению этого сигнала, чтобы это исправить (нажав правой кнопкой мыши, и выбрав Go To Source Code
):
module vector_abs(
input logic [31:0] x,
input logic [31:0] y,
output logic [31:0] abs
);
logic [31:0] min;
logic [31:0] min_half;
max_min max_min_unit(
.a(x),
.b(y),
.max(max),
.min(min)
);
//...
Это странно, курсор был установлен на строку .max(max)
, хотя раньше в этом случае курсор устанавливался на строку, где объявлялся выбранный сигнал. Но вот в чем дело, если мы просмотрим файл внимательно, то не обнаружим объявления сигнала вовсе. Как так вышло, что мы использовали необъявленный сигнал, а САПР не выдал нам ошибку? Дело в том, что стандарт IEEE 1364-2005 для языка SystemVerilog допускает подобное использование необъявленного сигнала. В этом случае, синтезатор неявно создаст одноименный одноразрядный сигнал, что и произошло.
Для исправления этой ошибки, объявим сигнал max
с корректной разрядностью и повторим моделирование.
Рисунок 27. Результат моделирования после объявления пропущенного сигнала.
Самостоятельная работа
Число ошибок сократилось до 40! Мы явно на верном пути. Повторяем предыдущие шаги, вернувшись к первой ошибке:
Рисунок 28. Первая ошибка в повторном моделировании.
В этот раз первая ошибка осталась прежней, только теперь схема считает, что результат должен равняться шести (в прошлый раз схема выдавала 2
). Мы уже убедились, что в этом случае модель дает правильный результат, поэтому сразу перейдем к формирующим результат сигналам:
Рисунок 29. Поведение внутренних сигналов модуля vector_abs
на временной диаграмме.
Видим, что значение сигнала min_half
, формирующего значение выхода abs
неверно (минимумом из 3
и 4
является 3
, 3/2 = 1
).
Не отходя далеко от кассы, мы замечаем, что значение min
, формирующее сигнал min_half
неверно: его значение 4
, а должно быть 3
.
Используя файлы исходного кода проекта, попробуйте разобраться в последней обнаруженной нами ошибке.